JavaScript Modules
For a long time, JavaScript had no built-in module system.
In the early days, we wrote our JavaScript programs in .js
files, and loaded them all up via <script>
tags in our HTML files. This worked alright, but it meant that every script shared the same environment; variables declared in one file could be accessed in another. It was a bit of a mess.
Several third-party solutions were invented, around 2009-2010. The most popular were RequireJS and CommonJS. CommonJS is the module.exports
thing you might've seen in back-end Node.js code.
Starting in the mid-2010s, however, JS got its own native module system! And it's pretty cool.
Basic premise
When we work with a JS module system, every file becomes a "module". A module is a JavaScript file that can contain one or more exports. We can pull the code from one module into another using the import
statement.
If we don't export a piece of data from one module, it won't be available in any other modules. The only way to share anything between modules is through import/export.
In addition to the code we write in our codebase, we can also import third-party modules like React.
The benefit of this rather complex system is that everything is isolated by default. We have to go out of our way to share data between modules. Imports/exports are the "bridges" between modules, and by strategically placing them, we can make complex programs easier to reason about.
Named exports
Each file can define one or more named exports:
Code Playground
In this case, our data.js
module uses the export
keyword to make a piece of data, significantNum
, available to other files.
We're also exporting a function, doubleNum
. We can export any JavaScript data type, including functions and classes.
In our main file, index.js
, we're importing both of these exports:
import { significantNum, doubleNum } from './data';
Inside the curly braces, we list each of the imports we want to bring in, by name. We don't have to import all of the exports, we can pick just the ones we need.
The string at the end, './data'
, is the path to the module. We're allowed to omit the .js
suffix, since it's implied.
The module system uses a linux-style relative path system to locate modules. A single dot, .
, refers to the same directory. Two dots, ..
, refers to a parent directory. If you're not familiar with this system, check out this quick primer.
Export statements
It's conventional to export variables as they're declared, like this:
export const significantNum = 5;
It's also possible to export previously-declared variables using squiggly brackets, like this:
const significantNum = 5;
export { significantNum };
Curiously, this syntax is quite rare — I only recently learned it was possible to do this! When it comes to named exports, it's much more common to export them right when they're declared.
We can also export functions as they're declared, like this:
// Produces a named export called `someFunction`:export function someFunction() { /* ... */ }
Renaming imports
Sometimes, we'll run into naming collisions with named imports:
import { Wrapper } from './Header';import { Wrapper } from './Footer';// 🚫 Identifier 'Wrapper' has already been declared.
This happens because named exports don't have to be globally unique. It's perfectly valid for both Header
and Footer
to use the same name:
// Header.jsexport function Wrapper() { return <header>Hello</header>;}
// Footer.jsexport function Wrapper() { return <footer>World</footer>;}
We can rename imports with the as
keyword:
import { Wrapper as HeaderWrapper } from './Header';import { Wrapper as FooterWrapper } from './Footer';// ✅ No problems
Within the scope of this module, HeaderWrapper
will refer to the Wrapper
function exported from the /Header.js
module. Similarly, FooterWrapper
will refer to the other Wrapper
function exported from /Footer.js
.
Default exports
There's a separate type of export in JS modules: the default export.
Let's look at an example:
Code Playground
When it comes to default exports, we always export an expression:
// ✅ Correct:const hi = 5;export default hi;
// 🚫 Incorrectexport default const hi = 10;
Every JS module is limited to a single default export. For example, this is invalid:
const hi = 5;const bye = 10;
export default hi;export default bye;// 🚫 SyntaxError: Too many default exports!
When importing default exports, we don't use squiggly brackets:
// ✅ Correct:import magicNumber from './data';
// 🚫 Incorrect:import { magicNumber } from './data';
When we're importing a default export, we can name it whatever we want; it doesn't have to match:
// ✅ This worksimport magicNumber from './data';
// ✅ This also works!import helloWorld from './data';
A lot of these differences might seem arbitrary or confusing, but they all stem from this fact: Every module can have multiple named exports, but only a single default export.
For example, we need to use the correct name when importing a named export because we need to specify which export we want! But because there's only a single default export, we can name it whatever we want, there's no ambiguity.
When to use which
Now that we've covered the fundamentals about named and default exports, you might be wondering: when should I use each type?
In fact, this is a question with no objectively “correct” answer. It all comes down to picking a convention that works for you.
For example, let's say that we have a file that holds a bunch of theme constants (colors and sizes and fonts and stuff). We could structure it like this:
Code Playground
Or, we could use named exports:
Code Playground
We can use either a single "grouped" default export, or lots of individual named exports. Both of these options are perfectly valid.
Here's a convention I like to follow, though: if a file has one obvious "main" thing, I make it the default export. Secondary things, like helpers and metadata, can be exported using named exports.
For example, a React component might be set up like this:
// components/Header.jsexport const HEADER_HEIGHT = '5rem';
function Header() { return ( <header style={{ height: HEADER_HEIGHT }}> {/* Stuff here */} </header> )}
export default Header;
The main “thing” in this Header.js file is the Header
component, and so it uses the default export. Anything else will use named exports.